#include <base_definition.h>
#include <misc.h>
#include <boot_ld.h>
#include <page.h>
#include <config.h>
#include <bootinfo.h>
#include <lm.h>
#include <video.h>

extern void LoaderMain(void);
static void InitVar(void);
static void EnableA20(void);
static void SetupGDT(void);
static result SetVideoMode(void);
static void SetupPT(void);
static u32 detectMemory(void);

SMAPEntry mm_buffer[MM_MAX_ENTRIES];
u32 memory_entries_num;
VgaInfoBlock *vib;
ModeInfoBlock *mib;
bootinfo *pbootinfo;

u64 *PML4T;
u64 *PDPT1;
u64 *PDPT2;
u64 *PDT1;
u64 *PDT2;

noreturn void LoaderMain(void)
{
    EnableA20();
    InitVar();
    SetupPT();
    SetupGDT();

    if (SetVideoMode() != SUCCESS)
        panic("[PANIC] Set vedio mode error");
    if (diskRead(kernel_sec_nm, KERNEL_BASE, kernel_start_sec) != SUCCESS)
        panic("[PANIC] Load kernel error: Disk read failed");
    if (detectMemory() != SUCCESS)
        panic("[PANIC] E820 error");

    println("[INFO ] Jumping to protect mode");

    Selector cspm, cslm, ds;
    pbootinfo->cspm = cspm = GetSelector(1);
    pbootinfo->cslm = cslm = GetSelector(2);
    pbootinfo->ds = ds = GetSelector(3);

    GdtPtr gdtptr;
    gdtptr.limit = sizeof(pbootinfo->gdt) - 1;
    gdtptr.base = (ptr) & (pbootinfo->gdt);

    asm volatile("jmp pmjump" ::"a"((u32)&gdtptr), "b"((u16)cspm), "c"((u16)cslm), "d"((u16)ds));
    die();
}

void InitVar(void)
{
    vib = alloc(sizeof(VgaInfoBlock), 1);
    mib = alloc(sizeof(ModeInfoBlock) * 64, 1);
    pbootinfo = (bootinfo *)BOOTINFO;
    PML4T = alloc(sizeof(u64) * PT_MAX_ENTRIES, 4096);
    PDPT1 = alloc(sizeof(u64) * PT_MAX_ENTRIES, 4096);
    PDPT2 = alloc(sizeof(u64) * PT_MAX_ENTRIES, 4096);
    PDT1 = alloc(sizeof(u64) * PT_MAX_ENTRIES, 4096);
    PDT2 = alloc(sizeof(u64) * PT_MAX_ENTRIES, 4096);

    init(*PML4T, sizeof(u64) * PT_MAX_ENTRIES);
    init(*PDPT1, sizeof(u64) * PT_MAX_ENTRIES);
    init(*PDPT2, sizeof(u64) * PT_MAX_ENTRIES);
    init(*PDT1, sizeof(u64) * PT_MAX_ENTRIES);
    init(*PDT2, sizeof(u64) * PT_MAX_ENTRIES);
    init(mm_buffer, sizeof(mm_buffer));
    init(*vib, sizeof(VgaInfoBlock));
    init(*mib, sizeof(ModeInfoBlock) * 64);
    init(*(bootinfo *)pbootinfo, sizeof(bootinfo));
}

result detectMemory(void)
{
    u32 contID = 0;
    u32 entries = 0, signature, bytes;
    SMAPEntry *buffer = mm_buffer;
    do
    {
        asm volatile("int $0x15"
                     : "=a"(signature), "=c"(bytes), "=b"(contID)
                     : "a"(0xE820), "b"(contID), "c"(24), "d"(0x534D4150), "D"(buffer));
        if (signature != 0x534D4150)
            return ERROR; // error
        if (bytes > 20 && (buffer->ACPI & 0x0001) == 0)
        {
            // ignore this entry
        }
        else
        {
            buffer++;
            entries++;
        }
    } while (contID != 0 && entries < MM_MAX_ENTRIES);
    pbootinfo->mm_entries_num = memory_entries_num = entries;
    pbootinfo->mm_buffer = (ptr)&mm_buffer;
    return SUCCESS;
}

void EnableA20(void)
{
    asm volatile(
        "inb $0x92, %%al;"
        "orb $0x02, %%al;"
        "andb $(~0x01), %%al;"
        "outb %%al, $0x92" ::
            : "%al", "cc");
}

void SetupGDT(void)
{
    InitGdtEntry(&(pbootinfo->gdt[1]), SD_G | SD_B | SD_P | SD_DPL0 | SD_S | SD_X | SD_R);
    InitGdtEntry(&(pbootinfo->gdt[2]), SD_G | SD_L | SD_P | SD_DPL0 | SD_S | SD_X | SD_R);
    InitGdtEntry(&(pbootinfo->gdt[3]), SD_G | SD_B | SD_P | SD_DPL0 | SD_S | SD_W);
}

result SetVideoMode(void)
{
    // TODO: 中断调用重置了GDT与段界限，但似乎无害
    u32 x = 1400, y = 900, d = 32;
    u16 r;
    u32 bestdiff = (u32)(-1);

    r = 0x4F00;
    set_es(((ptr)vib & 0xFFFF0000) >> 4);
    asm volatile("int $0x10"
                 : "+a"(r)
                 : "D"((u16)(ptr)vib));
    set_es(0);
    if (r != 0x004F)
        return ERROR;

    u16 *modes = (u16 *)(ptr)(((u16)vib->VideoModePtr) + ((vib->VideoModePtr & 0xFFFF0000) >> 12));
    for (u16 i = 0; i < 64 && modes[i] != 0xFFFF; i++)
    {
        r = 0x4F01;
        set_es((u16)(((ptr)(&mib[i]) & 0xFFFF0000) >> 4));
        asm volatile("int $0x10"
                     : "+a"(r)
                     : "c"(modes[i]), "D"((u16)(ptr)(&mib[i])));
        set_es(0);
    }

    u16 best = 280;
    u16 bestindex = 0;
    for (u16 i = 0; i < 64 && modes[i] != 0xFFFF; i++)
    {
        // Check if this is a graphics mode with linear frame buffer support
        if ((mib[i].ModeAttributes & 0x90) != 0x90)
            continue;

        // Check if this is a packed pixel or direct color mode
        if (mib[i].MemoryModel != 4 && mib[i].MemoryModel != 6)
            continue;

        // 只接受32位像素
        if (mib[i].BitsPerPixel != d)
            continue;

        // Check if this is exactly the mode we're looking for
        if (x == mib[i].XResolution && y == mib[i].YResolution && d == mib[i].BitsPerPixel)
        {
            best = modes[i];
            bestindex = i;
            bestdiff = 0;
            break;
        }

        // Otherwise, compare to the closest match so far, remember if best
        u32 di = diff(mib[i].XResolution * mib[i].YResolution, x * y);
        if (di < bestdiff)
        {
            best = modes[i];
            bestindex = i;
            bestdiff = di;
        }
    }

    print("[INFO ] Use mode ");
    print_u16(best);
    println();
    r = 0x4F02;
    asm volatile("int $0x10"
                 : "+a"(r)
                 : "b"(best));
    if (r != 0x004F)
        return ERROR;

    // ((bootinfo*)pbootinfo)->mib = mib[bestindex];
    // Gcc生成了迷之操作，我完全搞不清是我错了还是它错了
    memcpy((ptr)&mib[bestindex], (ptr)&pbootinfo->mib, sizeof(ModeInfoBlock));

    return SUCCESS;
}

void SetupPT(void)
{
    PML4T[0] = (ptr)(&PDPT1[0]) | PT_PRESENT | PT_RW;
    for (u32 i = 0; i < PT_LDMAPSIZE; i++)
    {
        PDPT1[i] = (i * 0x40000000) | PT_PRESENT | PT_RW | PT_PS;
    }

    PML4T[256] = (ptr)(&PDPT2[0]) | PT_PRESENT | PT_RW;
    PDPT2[0] = (ptr)(&PDT2[0]) | PT_PRESENT | PT_RW;
    for (u32 i = 0; i < PT_KNMAPSIZE; i++)
    {
        PDT2[i] = (i * 0x200000 + KERNEL_PMSTART) | PT_PRESENT | PT_RW | PT_PS;
    }

    asm volatile(
        "movl	%0, %%cr3 ;" ::"r"((u32)((ptr)(PML4T)&0x0000FFFFFFFFF000)));
}
